/*
 * MIT License
 *
 * Copyright (c) 2023-present, tangli
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package tony.redis.aspect

import com.fasterxml.jackson.databind.JavaType
import org.aspectj.lang.JoinPoint
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.After
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.reflect.MethodSignature
import org.springframework.expression.EvaluationException
import org.springframework.expression.spel.standard.SpelExpressionParser
import org.springframework.expression.spel.support.StandardEvaluationContext
import tony.annotation.redis.RedisCacheEvict
import tony.annotation.redis.RedisCacheMapEvict
import tony.annotation.redis.RedisCacheable
import tony.annotation.redis.RedisCacheableMap
import tony.core.PROJECT_GROUP
import tony.core.SpringContexts
import tony.core.exception.ApiException
import tony.core.utils.asToNotNull
import tony.core.utils.isBooleanType
import tony.core.utils.isDateTimeLikeType
import tony.core.utils.isNumberTypes
import tony.core.utils.isStringLikeType
import tony.core.utils.jsonToObj
import tony.core.utils.rawClass
import tony.core.utils.secondOfTodayRest
import tony.core.utils.toJavaType
import tony.redis.RedisManager
import tony.redis.toNum

/**
 * 默认 Redis 缓存切面实现。
 *
 * 通过 AOP 拦截 @RedisCacheable/@RedisCacheEvict 注解，实现方法级别的 Redis 缓存和自动过期。
 * 支持 SpEL 表达式动态生成缓存 key，支持多种序列化方式（Jackson/Protostuff等）。
 *
 * 注意事项：
 * - 不支持缓存返回类型为日期时间类（如 LocalDateTime、Date 等）的方法。
 * - 若缓存 key 生成表达式或参数异常，会抛出 ApiException。
 * - 建议业务方法幂等，避免缓存击穿带来的并发问题。
 *
 * @author tangli
 * @date 2023/09/28 19:55
 */
@Aspect
public abstract class RedisCacheAspect {
    /**
     * SpEL 表达式解析器。
     */
    private val expressionParser = SpelExpressionParser()

    /**
     * 执行缓存删除（@[RedisCacheEvict]）。
     *
     * @param joinPoint AOP 连接点，代表被拦截的方法调用
     * @throws ApiException 表达式解析或 key 生成异常时抛出
     * @author tangli
     * @date 2023/09/13 19:43
     */
    @After(
        "@annotation($PROJECT_GROUP.annotation.redis.RedisCacheEvict.Container) || @annotation($PROJECT_GROUP.annotation.redis.RedisCacheEvict)"
    )
    public fun doCacheEvict(joinPoint: JoinPoint) {
        val arguments = joinPoint.args
        val methodSignature = joinPoint.signature.asToNotNull<MethodSignature>()
        val paramsNames = methodSignature.parameterNames
        val annotations = methodSignature.method.getAnnotationsByType(RedisCacheEvict::class.java)
        annotations.forEach { annotation ->
            val cacheKey = generateCacheKey(arguments, paramsNames, annotation.keyTemplate)
            RedisManager.delete(cacheKey)
        }
    }

    /**
     * 执行缓存删除（@[RedisCacheMapEvict]）。
     *
     * @param joinPoint AOP 连接点，代表被拦截的方法调用
     * @throws ApiException 表达式解析或 key 生成异常时抛出
     * @author tangli
     * @date 2023/09/13 19:43
     */
    @After(
        "@annotation($PROJECT_GROUP.annotation.redis.RedisCacheMapEvict.Container) || @annotation($PROJECT_GROUP.annotation.redis.RedisCacheMapEvict)"
    )
    public fun doCacheMapEvict(joinPoint: JoinPoint) {
        val arguments = joinPoint.args
        val methodSignature = joinPoint.signature.asToNotNull<MethodSignature>()
        val paramsNames = methodSignature.parameterNames
        val annotations = methodSignature.method.getAnnotationsByType(RedisCacheMapEvict::class.java)
        annotations.forEach { annotation ->
            val key = generateCacheKey(arguments, paramsNames, annotation.keyTemplate)
            val hashKey = generateCacheKey(arguments, paramsNames, annotation.hashKeyTemplate)
            RedisManager.maps.delete(key, hashKey)
        }
    }

    /**
     * 方法缓存处理（@[RedisCacheable]）。
     *
     * 先查 Redis 缓存，命中则直接返回，否则执行方法并写入缓存。
     *
     * @param joinPoint AOP 连接点，代表被拦截的方法调用
     * @param annotation RedisCacheable 注解实例
     * @return 方法执行结果或缓存值
     * @throws ApiException 返回类型为日期时间类或表达式/key 生成异常时抛出
     * @author tangli
     * @date 2023/09/13 19:43
     */
    @Around("@annotation(annotation)")
    public fun doCacheable(
        joinPoint: ProceedingJoinPoint,
        annotation: RedisCacheable,
    ): Any? {
        val methodSignature = joinPoint.signature.asToNotNull<MethodSignature>()
        val key = generateCacheKey(joinPoint.args, methodSignature.parameterNames, annotation.keyTemplate)
        val timeout = getExpire(annotation.expire)
        val javaType = methodSignature.method.genericReturnType.toJavaType()
        if (javaType.isDateTimeLikeType()) {
            throw ApiException("Not support dateTimeLike type.")
        }
        return getCachedValueByType(key, javaType) ?: joinPoint
            .proceed()
            ?.also { result ->
                RedisManager.values.set(key, result, timeout)
            }
    }

    /**
     * 方法缓存处理（@[RedisCacheableMap]）。
     *
     * 先查 Redis 缓存，命中则直接返回，否则执行方法并写入缓存。
     *
     * @param joinPoint AOP 连接点，代表被拦截的方法调用
     * @param annotation RedisCacheable 注解实例
     * @return 方法执行结果或缓存值
     * @throws ApiException 返回类型为日期时间类或表达式/key 生成异常时抛出
     * @author tangli
     * @date 2023/09/13 19:43
     */
    @Around("@annotation(annotation)")
    public fun doCacheableMap(
        joinPoint: ProceedingJoinPoint,
        annotation: RedisCacheableMap,
    ): Any? {
        val methodSignature = joinPoint.signature.asToNotNull<MethodSignature>()
        val key = generateCacheKey(joinPoint.args, methodSignature.parameterNames, annotation.keyTemplate)
        val hashKey = generateCacheKey(joinPoint.args, methodSignature.parameterNames, annotation.hashKeyTemplate)
        val javaType = methodSignature.method.genericReturnType.toJavaType()
        if (javaType.isDateTimeLikeType()) {
            throw ApiException("Not support dateTimeLike type.")
        }
        return getCachedValueByType(key, hashKey, javaType) ?: joinPoint
            .proceed()
            ?.also { result ->
                RedisManager.maps.put(key, hashKey, result)
            }
    }

    /**
     * 获取缓存中的值，并根据返回类型做类型转换。
     *
     * 由子类实现，支持不同序列化方式。
     *
     * @param cacheKey 缓存 key
     * @param javaType 返回值类型
     * @return 缓存中的值，若无则返回 null
     */
    protected abstract fun getCachedValueByType(
        cacheKey: String,
        javaType: JavaType,
    ): Any?

    /**
     * 获取缓存中的值，并根据返回类型做类型转换。
     *
     * 由子类实现，支持不同序列化方式。
     *
     * @param cacheKey 缓存 key
     * @param javaType 返回值类型
     * @return 缓存中的值，若无则返回 null
     */
    protected abstract fun getCachedValueByType(
        cacheKey: String,
        cacheHashKey: String,
        javaType: JavaType,
    ): Any?

    /**
     * 获取过期
     * @param [expire] 到期
     * @return [Long]
     * @author tangli
     * @date 2025/09/01 11:41
     */
    private fun getExpire(expire: Long): Long =
        when (expire) {
            RedisCacheable.TODAY_END -> secondOfTodayRest()
            else -> expire
        }

    /**
     * 生成缓存 key。
     *
     * 支持在 keyTemplate 中使用 #{} 语法嵌入 SpEL 表达式，如：
     * - "user:#{userId}" -> "user:123"
     * - "order:#{order.id}:#{status}" -> "order:456:PAID"
     *
     * @param arguments 方法实际参数
     * @param paramsNames 方法参数名数组
     * @param keyTemplate 缓存 key 模板
     * @return 最终缓存 key
     * @throws ApiException SpEL 表达式解析失败时抛出
     */
    private fun generateCacheKey(
        arguments: Array<Any?>,
        paramsNames: Array<String>,
        keyTemplate: String,
    ): String {
        val template = SpringContexts.Env.resolvePlaceholders(keyTemplate)

        if (paramsNames.isEmpty() || !template.contains("#{")) {
            return template
        }

        val paramMap =
            paramsNames.foldIndexed<String, MutableMap<String, Any?>>(mutableMapOf()) { index, map, paramName ->
                map[paramName] = arguments[index]
                map
            }

        return resolveSpelTemplate(template, paramMap)
    }

    private val spelPattern = Regex("#\\{([^}]+)}")

    /**
     * 解析包含 #{} 语法的模板字符串。
     *
     * @param template 包含 SpEL 表达式的模板
     * @param paramMap 参数名到参数值的映射
     * @return 解析后的字符串
     * @throws ApiException SpEL 表达式解析失败时抛出
     */
    private fun resolveSpelTemplate(
        template: String,
        paramMap: Map<String, Any?>,
    ): String =
        try {
            spelPattern.replace(template) { matchResult ->
                val expression = matchResult.groupValues[1]
                val stringList = expression.split(".")
                if (stringList.size < 2) {
                    paramMap[expression].toString()
                } else {
                    val paramName = stringList[0]
                    val realExpression = expression.replace("$paramName.", "")
                    val spelExpression = expressionParser.parseExpression(realExpression)
                    val result = spelExpression.getValue(StandardEvaluationContext(paramMap[paramName]))
                    result?.toString() ?: ""
                }
            }
        } catch (e: EvaluationException) {
            throw ApiException(
                "Template=$template SpEL=${e.expressionString} " +
                    "expression evaluation failed: ${e.message}",
                cause = e
            )
        } catch (e: Exception) {
            throw ApiException("Template=$template resolution failed: ${e.message}", cause = e)
        }
}

/**
 * Jackson Redis 缓存切面实现。
 *
 * 支持字符串、数字、布尔、枚举、对象等多种类型的缓存序列化与反序列化。
 *
 * @author tangli
 * @date 2023/09/28 19:55
 */
internal class JacksonRedisCacheAspect : RedisCacheAspect() {
    override fun getCachedValueByType(
        cacheKey: String,
        javaType: JavaType,
    ): Any? =
        RedisManager.values.get<String>(cacheKey)?.let { cachedValue ->
            fromCacheValue(cachedValue, javaType)
        }

    override fun getCachedValueByType(
        cacheKey: String,
        cacheHashKey: String,
        javaType: JavaType,
    ): Any? =
        RedisManager.maps.get<String>(cacheKey, cacheHashKey)?.let { cachedValue ->
            fromCacheValue(cachedValue, javaType)
        }

    private fun fromCacheValue(
        cachedValue: String,
        javaType: JavaType,
    ): Any? =
        when {
            javaType.isStringLikeType() -> {
                cachedValue
            }

            javaType.isNumberTypes() -> {
                cachedValue.toNum(javaType.rawClass())
            }

            javaType.isBooleanType() -> {
                cachedValue.toBooleanStrictOrNull()
            }

            javaType.isEnumImplType -> {
                javaType.rawClass.enumConstants.firstOrNull {
                    cachedValue == it.toString()
                }
            }

            else -> {
                cachedValue.jsonToObj(javaType)
            }
        }
}
